package com.wiscamp.ninechapters.problems.domain.services;

import com.wiscamp.ninechapters.common.GlobalState;
import com.wiscamp.ninechapters.common.core.CodeHelper;
import com.wiscamp.ninechapters.common.core.DateTimeHelper;
import com.wiscamp.ninechapters.common.core.IOHelper;
import com.wiscamp.ninechapters.common.core.RandomHelper;
import com.wiscamp.ninechapters.common.exception.ExceptionHandler;
import com.wiscamp.ninechapters.common.exception.ServiceException;
import com.wiscamp.ninechapters.problems.domain.ProblemsErrorCodes;
import com.wiscamp.ninechapters.problems.domain.gateway.QiniuGateway;
import com.wiscamp.ninechapters.problems.domain.gateway.TagGateway;
import com.wiscamp.ninechapters.problems.domain.gateway.UserGateway;
import com.wiscamp.ninechapters.problems.domain.models.problems.ImageHelper;
import com.wiscamp.ninechapters.problems.domain.models.problems.problems.Problem;
import com.wiscamp.ninechapters.problems.domain.models.sets.KnowledgePointRule;
import com.wiscamp.ninechapters.problems.domain.models.sets.ProblemSet;
import com.wiscamp.ninechapters.problems.domain.models.sets.RegroupRule;
import com.wiscamp.ninechapters.problems.domain.models.sets.SetProblem;
import com.wiscamp.ninechapters.problems.domain.repositories.ProblemCollectionRepository;
import com.wiscamp.ninechapters.problems.domain.repositories.ProblemRepository;
import com.wiscamp.ninechapters.problems.domain.repositories.ProblemSetRepository;
import com.wiscamp.ninechapters.problems.enums.SetTypes;
import com.wiscamp.ninechapters.problems.infrastructure.po.problems.QuestionOptionPO;
import com.wiscamp.ninechapters.problems.infrastructure.po.sets.TagRulePO;
import cube.common.cache.CacheManagerNames;
import cube.common.core.CodeUtils;
import cube.ddd.aspect.HandleServiceException;
import cube.ddd.domain.BaseDomainService;
import cube.ddd.exception.ExceptionUtils;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.apache.commons.text.StringEscapeUtils;
import org.jsoup.Jsoup;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Repository
public class ProblemSetService extends BaseDomainService {

    private final ProblemSetRepository problemSetRepository;

    private final ProblemRepository problemRepository;

    private final RegroupRuleService regroupRuleService;

    private final ProblemCollectionRepository problemCollectionRepository;

    private final UserGateway userGateway;
    private final TagGateway tagGateway;
    private final QiniuGateway qiniuGateway;

    private final String CACHE_NAME = "problemSets";

    public ProblemSetService(ProblemSetRepository problemSetRepository, ProblemRepository problemRepository, RegroupRuleService regroupRuleService, ProblemCollectionRepository problemCollectionRepository, UserGateway userGateway, TagGateway tagGateway, QiniuGateway qiniuGateway) {
        this.problemSetRepository = problemSetRepository;
        this.problemRepository = problemRepository;
        this.regroupRuleService = regroupRuleService;
        this.problemCollectionRepository = problemCollectionRepository;
        this.userGateway = userGateway;
        this.tagGateway = tagGateway;
        this.qiniuGateway = qiniuGateway;
    }


    // region set service

    /**
     * Gets a problem set
     *
     * @param setId
     * @return
     */

    @Cacheable(value = CACHE_NAME, key = "#setId")
    @HandleServiceException
    public ProblemSet getProblemSet(@Min(0) long setId) {
        var set = problemSetRepository.getProblemSet(setId);
        if (Objects.isNull(set)) {
            ExceptionUtils.throwServiceException(ProblemsErrorCodes.PROBLEM_SET_NOT_FOUND, setId);
        }
        return set;
    }

    /*@HandleServiceException
    public ProblemSet getProblemSetByCode(@NotBlank String code, @Min(0) long organizationId) {
        return problemSetRepository.getProblemSetByCode(code, organizationId);
    }*/

    /*@HandleServiceException
    public Collection<ProblemSet> getProblemSetsByName(@NotBlank String name, @Min(0) long organizationId) {
        return problemSetRepository.getProblemSetsByName(name, organizationId);
    }*/

    /*@HandleServiceException
    public Collection<ProblemSet> getProblemSetsByOrganization(@Min(0) long organizationId) {
        return problemSetRepository.getProblemSetsByOrganization(organizationId);
    }*/

    /**
     * Creates a problem set
     *
     * @param set
     * @return
     */
    @CachePut(value = CACHE_NAME, key = "#set.setId", cacheManager = CacheManagerNames.SIXTY_MINUTES_CACHE)
    @HandleServiceException
    public ProblemSet createProblemSet(@NotNull ProblemSet set, @Min(0) long operatorId) {
        var existedSet = problemSetRepository.getProblemSetByCode(set.getSetCode(), set.getOrganizationId());
        if (Objects.nonNull(existedSet)) {
            ExceptionUtils.throwServiceException(ProblemsErrorCodes.PROBLEM_SET_CODE_EXISTS, set);
        }
        set.createBy(operatorId);
        return problemSetRepository.saveProblemSet(set);
    }

    /**
     * Updates a problem set
     *
     * @param set
     * @return
     */

    @CachePut(value = CACHE_NAME, key = "#set.setId", cacheManager = CacheManagerNames.SIXTY_MINUTES_CACHE)
    @HandleServiceException
    public ProblemSet updateProblemSet(@NotNull ProblemSet set, @Min(0) long operatorId) {
        var existedSet = getProblemSet(set.getSetId());
        existedSet = problemSetRepository.getProblemSetByCode(set.getSetCode(), set.getOrganizationId());
        if (Objects.nonNull(existedSet) && existedSet.getSetId() != set.getSetId()
                && existedSet.getSetCode().equalsIgnoreCase(set.getSetCode())) {
            ExceptionUtils.throwServiceException(ProblemsErrorCodes.EXAM_CODE_EXISTS, set);
        }
        set.updateBy(operatorId);
        return problemSetRepository.saveProblemSet(set);
    }

    /**
     * Deletes a problem set
     *
     * @param setId
     * @return
     */
    @CacheEvict(value = CACHE_NAME, key = "#setId")
    @HandleServiceException
    public boolean deleteProblemSet(@Min(0) long setId, @Min(0) long operatorId) {
        var set = getProblemSet(setId);
        if (Objects.isNull(set)) return false;
        // 记录操作日志
        set.delete();
        set.updateBy(operatorId);
        problemSetRepository.saveProblemSet(set);
        return true;
    }


    /**
     * Gets today's assigned items by a user
     * 获取今日已分配給该学员的题目编号列表
     *
     * @param userId
     * @return
     */
    public Collection<Long> getAssignedProblemsByUser(@Min(0) long userId) {
        var today = LocalDate.now().atTime(0,0,0);
        var tomorrow = today.plusDays(1);
        Collection<Long> setIds = getProblemSetIdsByUser(userId, today, tomorrow);
        if (setIds.isEmpty())
            return new ArrayList<>();
        return getProblemIdsBySets(setIds);
    }

    /**
     * Creates a problem set by a regroup rule
     * 根据组题规则为用户创建习题集
     *
     * @param ruleId
     * @param creatorId
     * @return
     */
    @Transactional(transactionManager = "problemsTransactionManager")
    @HandleServiceException
    public ProblemSet createProblemSetByRule(@Min(0) long ruleId, @Min(0) long creatorId) {
        // validate rule
        var rule = regroupRuleService.getRegroupRule(ruleId);
        if (rule == null) {
            var errorCode = "service-exception.regroup-rule.id.notfound";
            throw new ServiceException(null, errorCode, ruleId);
        }

        long entityId = GlobalState.ENTITY_PROBLEM_ID;
        // 标签规则匹配题目编号列表
        Collection<Long> tagRuleItems;
        // 知识点匹配题目编号列表
        Collection<Long> pointRuleItems;
        // 结果题目编号列表
        Collection<Long> problemIds = new HashSet<>();

        if (rule.isHasKnowledgePointRule()) {
            // 规则知识点关联题Id列表
            pointRuleItems = getProblemIdsByPointRule(entityId, rule);
        } else {
            pointRuleItems = null;
        }

        if (rule.isHasTagRule()) {
            // 去除本人当日已放入练习题集的题目，避免出重复题
            Collection<Long> assignedProblemIds = getAssignedProblemsByUser(creatorId);
            // 规则标签关联题Id列表
            tagRuleItems = getProblemIdsByTagRule(rule, assignedProblemIds, pointRuleItems);
            problemIds = tagRuleItems;
        }

        if (problemIds.size() == 0) {
            var errorCode = "service-exception.exercise-set.item.notenough";
            throw new ServiceException(null, errorCode, ruleId);
        }

        // 按难度对题目编号列表进行排序
        Collection<Long> sortedItemIds = problemRepository.getSortedProblemIdsByDifficulty(problemIds);
        var text = CodeHelper.get62RadixDateTime();
        var setName = rule.getRuleName() + "-" + text;
        return createProblemSetByProblemsAndRule(sortedItemIds, creatorId, "", setName, rule, rule.getTotalTimeLimit(), rule.getSetTypeId());
    }

    /**
     * Creates a problem set by problems
     *
     * @param problemIds
     * @param creatorId
     * @return
     */
    @HandleServiceException
    public ProblemSet createProblemSetByProblems(@NotEmpty Collection<Long> problemIds, @Min(0) long creatorId, @NotBlank String setCode, @NotBlank String setName,
                                                 @Min(0) int setTypeId, @Min(0) int totalTimeLimit) {
        return createProblemSetByProblemsAndRule(problemIds, creatorId, setCode, setName, null, totalTimeLimit, setTypeId);
    }

    @HandleServiceException
    public ProblemSet createProblemSetByProblemsAndRule(@NotBlank Collection<Long> problemIds, @Min(0) long creatorId, @NotBlank String setCode, @NotBlank String setName,
                                                        @NotNull RegroupRule rule, @Min(0) int totalTimeLimit, @Min(0) int setType) {
        var randomText = CodeUtils.get36RadixUpperDateTime();
        // create problem set
        var set = new ProblemSet();

        if (!setCode.isEmpty())
            set.setSetCode(setCode);
        else
            set.setSetCode("PS" + randomText);

        if (setName != null && !setName.isEmpty())
            set.setSetName(setName);
        else
            set.setSetName("PS" + randomText);

        if (rule != null) {
            set.setSetTypeId(rule.getSetTypeId());
            set.setTotalScore(rule.getTotalScore());
            set.setRuleId(rule.getRuleId());
        } else {
            if (setType > 0)
                set.setSetTypeId(setType);
            else
                set.setSetTypeId(SetTypes.EXERCISE.getValue());
        }
        if (totalTimeLimit > 0)
            set.setTotalTimeLimit(totalTimeLimit);
        else
            set.setTotalTimeLimit(0);

        long organizationId = userGateway.getOrganizationIdByUser(creatorId);
        set.setOrganizationId(organizationId);
        set.setSubjectId(GlobalState.SUBJECT_MATH_ID);
        set.setTotalScore(0);
        set.setProblemQuantity(problemIds.size());
        set.setDifficulty(0);
        set.setTrueProblem(false);
        set.setCreatorId(creatorId);
        set.setCreationTime(LocalDateTime.now());
        set.setModifierId(creatorId);
        set.setModificationTime(LocalDateTime.now());

        int counter = 1;
        for (long id : problemIds) {
            var setProblem = new SetProblem();
            setProblem.setProblemId(id);
            setProblem.setProblemIndex(counter);
            set.getSetProblems().add(setProblem);
            counter++;
        }
        // 计算习题集难度和时限
        calculateDifficultyAndTimeLimit(set, rule);
        return createProblemSet(set, creatorId);
    }

    private Collection<Long> getProblemIdsByPointRule(@Min(0) long problemEntityId, @NotNull RegroupRule rule) {
        Collection<Long> pointIds = rule.getKnowledgePointRules().stream().map(KnowledgePointRule::getPointId)
                .collect(Collectors.toList());
        // Gets sub points by parent point.
        pointIds = tagGateway.getChildKnowledgePointIds(pointIds);

        int difficulty = rule.getDifficulty();
        return problemRepository.getProblemIdsByPointAndDifficulty(pointIds, difficulty, 200);
    }

    private Collection<Long> getProblemIdsByPoints(@NotBlank Collection<Long> points) {
        Collection<Long> pointIds = tagGateway.getChildKnowledgePointIds(points);
        int difficulty = 0;
        return problemRepository.getProblemIdsByPointAndDifficulty(pointIds, difficulty, 200);
    }

    private Collection<Long> getProblemIdsByTagRules(@NotBlank Collection<TagRulePO> tagRules,
                                                     @NotBlank Collection<Long> pointRuleProblemIds, @Min(0) int quantity, @NotBlank Collection<Long> assignedProblemIds) {

        int start = 0;
        int end = 100;
        ArrayList<Long> tagIds = new ArrayList<>();

        for (var tr : tagRules) {
            tagIds.add(tr.getTagId());
            start = tr.getStartDifficulty();
            if (tr.getEndDifficulty() >= 1)
                end = tr.getEndDifficulty();
        }

        // 根据标签和难度获取匹配题目
        var itemsSet = problemRepository.getProblemIdsByTagAndDifficulty(tagIds, start, end);
        // 排除今日用户已分配题目
        if (assignedProblemIds.size() > 0)
            itemsSet.removeAll(assignedProblemIds);

        // 如果有知识点规则，和知识点关联题目取交集
        if (pointRuleProblemIds.size() > 0)
            itemsSet.retainAll(pointRuleProblemIds);
        // 如果tagRule已指定抽题数量则随机抽题，否则留待所有标签题目组合后再根据总题数抽题
        Collection<Long> tagCollection;
        if (quantity <= 0) quantity = 100;
        if (quantity <= itemsSet.size()) {
            tagCollection = RandomHelper.getRandomCollection(itemsSet, quantity);
        } else {
            tagCollection = RandomHelper.getRandomCollection(itemsSet, itemsSet.size());
        }
        HashSet<Long> resultSet = new HashSet<>(tagCollection);
        return resultSet;
    }

    /**
     * 根据问题编号列表创建新习题集或加入旧习题集
     *
     * @param dto
     * @param organizationId
     * @return
     */
    @HandleServiceException
    public ProblemSet createOrAddToProblemSetByProblems(@NotEmpty Collection<Long> problemIds, boolean isNewSet, String oldSetName, @Min(0) long organizationId, @Min(0) long operatorId) {
        if (!isNewSet) {
            var setName = oldSetName;
            Collection<ProblemSet> sets = problemSetRepository.getProblemSetsByName(setName, organizationId);
            if (sets.size() <= 0) {
                var errorCode = "service-exception.exercise-set.name.notenough";
                throw new ServiceException(null, errorCode, setName);
            }
            var set = (ProblemSet) sets.toArray()[0];
            int size = set.getSetProblems().size();
            for (long problemId : problemIds) {
                var setProblem = new SetProblem();
                setProblem.setSetId(set.getSetId());
                setProblem.setProblemId(problemId);
                size = size + 1;
                setProblem.setProblemIndex(size);
                set.getSetProblems().add(setProblem);
            }
            set.setProblemQuantity(set.getSetProblems().size());
            return updateProblemSet(set, 0);
        } else {
            var text = CodeHelper.get62RadixDateTime();
            var setCode = "PS" + text;
            var setName = "PS" + text;
            return createProblemSetByProblems(problemIds, operatorId, setCode, setName, 0, 0);
        }
    }

    /**
     * Gets id collection of items by tag rule
     *
     * @param rule
     * @return
     */
    private HashSet<Long> getProblemIdsByTagRule(@NotNull RegroupRule rule, @NotBlank Collection<Long> assignedProblemIds,
                                                 @NotBlank Collection<Long> pointRuleItemIds) {
        HashSet<Long> resultSet = new HashSet<>();
        boolean hasCustomTagItemQuantity = false;
        for (var tr : rule.getTagRules()) {
            ArrayList<Long> tagIds = new ArrayList<>();
            tagIds.add(tr.getTagId());
            int start = tr.getStartDifficulty();
            int end = tr.getEndDifficulty();
            if (end == 0) {
                if (rule.getDifficulty() > 0) {
                    start = rule.getDifficulty() - 11;
                    if (start < 0)
                        start = 0;
                    end = rule.getDifficulty() + 11;
                } else {
                    start = 0;
                    end = 200;
                }
            }
            // 根据标签和难度获取匹配题目
            var problemSet = problemRepository.getProblemIdsByTagAndDifficulty(tagIds, start, end);
            // 移除本人本日已出题目
            problemSet.removeAll(assignedProblemIds);
            // 如果有知识点规则，和知识点关联题目取交集
            if (pointRuleItemIds != null && pointRuleItemIds.size() > 0)
                problemSet.retainAll(pointRuleItemIds);
            // 如果tagRule已指定抽题数量则随机抽题，否则留待所有标签题目组合后再根据总题数抽题
            Collection<Long> tagCollection;
            if (tr.getProblemQuantity() > 0) {
                tagCollection = RandomHelper.getRandomCollection(problemSet, tr.getProblemQuantity());
                hasCustomTagItemQuantity = true;
            } else {
                tagCollection = RandomHelper.getRandomCollection(problemSet, problemSet.size());
            }
            resultSet.addAll(tagCollection);
        }
        // 如果tagRule没有指定抽题数量，则根据rule总题数抽题
        if (!hasCustomTagItemQuantity) {
            Collection<Long> tempIds = RandomHelper.getRandomCollection(resultSet, rule.getProblemQuantity());
            HashSet<Long> tempSet = new HashSet<>(tempIds);
            return tempSet;
        }
        return resultSet;
    }

    /**
     * Gets problem sets by a regroup rule
     *
     * @param ruleId
     * @return
     */

    @HandleServiceException
    public Collection<ProblemSet> getProblemSetsByRule(@Min(0) long ruleId) {
        return problemSetRepository.getProblemSetsByRule(ruleId);
    }

    /**
     * Calculates difficulty and time limit of a problem set
     *
     * @param set
     * @param rule
     */

    private void calculateDifficultyAndTimeLimit(@NotNull ProblemSet set, @NotNull RegroupRule rule) {
        int totalDifficulty = 0;
        double totalScore = 0;
        Collection<SetProblem> setProblems = set.getSetProblems();
        for (var setProblem : setProblems) {
            var problem = problemRepository.getProblem(setProblem.getProblemId());
            if (Objects.nonNull(problem)) {
                totalDifficulty = totalDifficulty + problem.getDifficulty();
                var problemScore = getProblemScoreByDifficulty(problem, rule);
                setProblem.setProblemScore(problemScore);
                totalScore = totalScore + problemScore;
            }
        }
        // 习题集的难度值为所有题目的平均难度
        set.setDifficulty(totalDifficulty / setProblems.size());

        // 如果组题规则设置总分则取规则设置，如果未设置习题集的总分数则根据难度计算总分
        if (rule != null && rule.getTotalScore() > 0) {
            set.setTotalScore(rule.getTotalScore());
        } else {
            set.setTotalScore(totalScore);
        }

        if (set.getTotalTimeLimit() == 0) {
            if (rule != null && rule.getTotalTimeLimit() > 0) {
                set.setTotalTimeLimit(rule.getTotalTimeLimit());
            } else {
                // 如果组题规则未设置时限，则每道题答题时间为5分钟
                set.setTotalTimeLimit(setProblems.size() * 5);
                // 如果组题规则未设置时限，则不设置答题时限
                // set.setTotalTimeLimit(0);
            }
        }
        // update item quantity
        set.setProblemQuantity(setProblems.size());
    }

    /**
     * Gets score of a correct item
     *
     * @param item
     * @param rule
     * @return
     */

    private double getProblemScoreByDifficulty(@NotNull Problem problem, @NotNull RegroupRule rule) {
        double score;
        if (rule != null && rule.getTotalScore() > 0) {
            score = rule.getTotalScore() / rule.getProblemQuantity();
        } else {
            // 根据难度值，每20分一个级别，题目分数从1递增
            score = problem.getDifficulty() / 20;
        }
        if (score <= 0)
            score = 1;
        return score;
    }

    /**
     * Creates a problem set by a item pool
     * 从题集创建习题集
     *
     * @param collectionCode
     * @param organizationId
     * @param creatorId
     * @return
     */
    @HandleServiceException
    public ProblemSet createProblemSetByCollection(@NotBlank String collectionCode, @Min(0) long organizationId, @Min(0) long creatorId) {


        var collection = problemCollectionRepository.getProblemCollectionByCode(collectionCode, organizationId);
        if (Objects.isNull(collection)) return null;

        var sets = problemSetRepository.getProblemSetsByName(collection.getCollectionName(), organizationId);
        if (!sets.isEmpty()) {
            return sets.stream().findFirst().orElse(null);
        }

        var problemIds = problemCollectionRepository.getProblemIdsByCollection(collection.getCollectionId());
        var text = CodeUtils.get36RadixUpperDateTime();
        var setName = collection.getCollectionName();
        return createProblemSetByProblems(problemIds, creatorId, "", setName, 0, collection.getTotalTimeLimit());
    }

    /**
     * Changes name of a problem set
     *
     * @param setId
     * @param setName
     * @return
     */
    @HandleServiceException
    public boolean changeProblemSetName(@Min(0) long setId, @NotBlank String setName, @Min(0) long operatorId) {
        var set = getProblemSet(setId);

        set.updateBy(operatorId);
        set.setSetName(setName);
        updateProblemSet(set, operatorId);
        return true;
    }

    /**
     * Creates a problem set by custom text
     * 通过定制文本列表创建习题集
     * 格式标准参考：题集类别-年份-题序号，如 AMC8-2017-3， AMC12A-2013-17
     *
     * @param setName
     * @param texts
     * @param organizationId
     * @param creatorId
     * @return
     */
    @Transactional(transactionManager = "problemsTransactionManager")
    @HandleServiceException
    public ProblemSet createProblemSetByCustomText(@NotBlank String setName, @NotBlank Collection<String> texts, @Min(0) long organizationId,
                                                   @Min(0) long creatorId) {
        Collection<Long> problemIds = new ArrayList<>();
        texts.forEach(text -> {
            long problemId = parseTextToProblemId(text, organizationId);
            problemIds.add(problemId);
        });
        return createProblemSetByProblems(problemIds, creatorId, "", setName, 0, 0);
    }

    /**
     * Parse item id from custom text
     *
     * @param text
     * @param organizationId
     * @return
     */
    private long parseTextToProblemId(@NotBlank String text, @Min(0) long organizationId) {
        String[] stringArray = text.split("-");
        var type = stringArray[0]; // 分类
        var year = stringArray[1]; // 年份
        var index = stringArray[2]; // 序号
        int problemIndex = Integer.parseInt(index);
        // 组合题集代码并查询题集
        var code = type + "-" + year;
        var collection = problemCollectionRepository.getProblemCollectionByCode(code, organizationId);
        if (Objects.isNull(collection)) return 0;
        return problemCollectionRepository.getProblemIdByIndex(collection.getCollectionId(), problemIndex);
    }

    /**
     * Gets problem sets created by a user
     * 根据创建人和时间区间获取习题集编号列表
     *
     * @param userId
     * @param startDate
     * @param endDate
     * @return
     */

    @HandleServiceException
    public Collection<Long> getProblemSetIdsByUser(@Min(0) long userId, LocalDateTime startDate, LocalDateTime endDate) {
        return problemSetRepository.getProblemSetIdsByCreatorAndDate(userId, startDate, endDate);
    }

    /**
     * Gets id collection of items by problem sets
     *
     * @param setIds
     * @return
     */
    @HandleServiceException
    public Collection<Long> getProblemIdsBySets(@NotBlank Collection<Long> setIds) {
        return problemSetRepository.getProblemIdsBySets(setIds);
    }

    /**
     * Gets today's problem set of daily challenge
     * 获取今日挑战习题集
     *
     * @return
     */
    @HandleServiceException
    public ProblemSet getProblemSetOfDailyChallenge(int grade) {
        var today = DateTimeHelper.getDateString(LocalDate.now(), "yyMMdd");
        var setCode = "DC-" + "G" + grade + "-" + today;
        var setName = "DailyChallenge-" + "G" + grade + "-" + today;
        var set = problemSetRepository.getProblemSetByCode(setCode, GlobalState.ORGANIZATION_ID);
        if (!Objects.isNull(set)) return set;

        // 获取历史中已用于每日挑战的题目编号列表
        Collection<Long> problemIds = getProblemIdsOfDailyChallengeInHistory();
        // 取3道题
        String tagGroupName = "DailyChallenge";
        String tagName = "MO-" + grade;
        int problemQty = 3;
        Collection<Long> moProblemIds = getProblemIdsByTagName(tagGroupName, tagName, problemQty, problemIds);
        Collection<Long> itemIds = new ArrayList<>();
        itemIds.addAll(moProblemIds);
        if (itemIds.size() <= 0) return null;
        int battleSetType = SetTypes.CHALLENGE.getValue();
        set = createProblemSetByProblems(itemIds, 1, setCode, setName, battleSetType, 0);
        return set;
    }

    /**
     * Gets id collection of some items by tag
     * 通过标签名获取匹配的题目编号列表
     *
     * @param groupName
     * @param tagName
     * @param quantity
     * @param historyProblemIds
     * @return
     */
    private Collection<Long> getProblemIdsByTagName(@NotBlank String groupName, @NotBlank String tagName, @Min(0) int quantity,
                                                    @NotBlank Collection<Long> historyProblemIds) {
        var tag = tagGateway.getTagByName(groupName, tagName);
        if (tag == null) return new HashSet<>();
        Collection<Long> tagIds = new ArrayList<>();
        tagIds.add(tag.getTagId());
        var problemIds = tagGateway.getProblemIdsByTags(tagIds);
        problemIds.removeAll(historyProblemIds);
        return RandomHelper.getRandomCollection(problemIds, quantity);
    }

    /**
     * Gets id collection of items in history problem sets daily challenge
     *
     * @return
     */
    private Collection<Long> getProblemIdsOfDailyChallengeInHistory() {
        return problemSetRepository.getProblemIdsByChallengeHistory();
    }

    /**
     * Removes item from problem set
     *
     * @param setId
     * @param problemId
     * @return
     */

    @HandleServiceException
    public boolean removeSetProblem(@Min(0) long setId, @Min(0) long problemId, @Min(0) long operatorId) {
        problemSetRepository.removeSetProblem(setId, problemId);
        return true;
    }

    /**
     * Exports a tex text file by a problem set
     * 导出习题集到tex排版文件，并压缩上传到七牛
     *
     * @param setId
     * @return
     */

    @HandleServiceException
    public String exportProblemSet(@Min(0) long setId) {
        BufferedWriter writer = null;
        try {
            var parentFolder = "/deploy/";
            var childFolder = "tex/";
            // String parentFolder = "d:\\temp\\";
            // String childFolder = "tex\\";
            var folderPath = parentFolder + childFolder;
            IOHelper.deleteFolder(folderPath);
            IOHelper.ensureFolder(folderPath);
            var set = getProblemSet(setId);
            var setName = set.getSetName();
            int timeLimit = set.getTotalTimeLimit();
            int itemQuantity = set.getProblemQuantity();
            var problemIds = problemSetRepository.getProblemIdsBySet(setId);
            Collection<Problem> problems = problemRepository.getProblemsByIds(problemIds);
            var builder = new StringBuilder();

            builder.append("\\documentclass[14pt, a4paper]{article}");
            builder.append("\r\n");
            builder.append("\\usepackage[utf8]{inputenc}");
            builder.append("\r\n");
            builder.append("\\documentclass[UTF8]{article}");
            builder.append("\r\n");
            builder.append("\\usepackage{CTEX}");
            builder.append("\r\n");
            builder.append("\\usepackage{comment}");
            builder.append("\r\n");
            builder.append("\\usepackage{graphicx}");
            builder.append("\r\n");
            builder.append("\\usepackage{geometry}");
            builder.append("\r\n");
            builder.append("\\geometry{left=1.5cm,right=1.5cm,top=2.5cm,bottom=2.5cm}");
            builder.append("\r\n");
            builder.append("\\usepackage{fancyhdr}");
            builder.append("\r\n");
            builder.append("\\pagestyle{fancy}");
            builder.append("\r\n");
            builder.append("\\fancyhf{}");
            builder.append("\r\n");
            builder.append("\\fancyhead[L]{" + setName + "}");
            builder.append("\r\n");
            builder.append("\\fancyhead[R]{wiscamp.com}");
            builder.append("\r\n");
            builder.append("\\fancyfoot[C]{\\thepage}");
            builder.append("\r\n");
            builder.append("\r\n");
            builder.append("\\begin{document}");
            builder.append("\r\n");
            builder.append("\\begin{enumerate}");
            builder.append("\r\n");
            problems.forEach(problem -> exportProblem(builder, problem, folderPath));
            builder.append("\\end{enumerate}");
            builder.append("\r\n");
            builder.append("\\end{document}");
            builder.append("\r\n");

            var texFile = new File(parentFolder + childFolder + setId + ".tex");
            writer = new BufferedWriter(new FileWriter(texFile));
            writer.write(builder.toString());
            writer.flush();
            writer.close();
            var zipFile = setId + "." + DateTimeHelper.getDateTimeString(LocalDateTime.now(), "yyMMddHHmmss") + ".zip1";
            var fullZipFile = parentFolder + zipFile;
            // compressFolder(new File(parentFolder + childFolder), parentFolder, zipFile);
            IOHelper.zipFolder(folderPath, parentFolder, zipFile);
            return uploadTexFile(fullZipFile);
        } catch (IOException iex) {
            ExceptionHandler.throwServiceException(iex, logger, ExceptionHandler.UNKNOWN_SERVICE_EXCEPTION_CODE, null);
        } catch (Exception ex) {
            if (ex instanceof ServiceException)
                throw ex;
            ExceptionHandler.throwServiceException(ex, logger, ExceptionHandler.UNKNOWN_SERVICE_EXCEPTION_CODE, null);
        }
        return "";
    }

    /**
     * Uploads a tex zip file
     *
     * @param fileName
     * @return
     */
    private String uploadTexFile(@NotBlank String fileName) {
        var texPath = "/tex/";
        return qiniuGateway.uploadFile(new File(fileName), texPath);
    }

    /**
     * Export a item to tex text
     * 导出题目到tex文件片段
     *
     * @param builder
     * @param item
     * @param targetFolder storage path of a image
     */
    private void exportProblem(@NotNull StringBuilder builder, @NotNull Problem item, @NotBlank String targetFolder) {
        /*try {
            Optional<QuestionPO> q = item.getQuestions().stream().findFirst();
            QuestionPO question = null;
            if (q.isPresent()) {
                question = q.get();
            }
            if (question == null) return;
            var questionText = question.getQuestionHypertext();
            builder.append("\\item " + convertToLatexText(questionText, true, targetFolder));
            builder.append("\r\n");
            builder.append("\\begin{description}");
            builder.append("\r\n");
            Collection<QuestionOptionPO> options = question.getOptions();
            int optionIndex = 1;
            //TODO: 这里可能需要针对题型导出TEX格式文件做相应处理
            for (var option : options) {
                exportOption(builder, option, optionIndex, targetFolder);
                optionIndex++;
            }
            builder.append("\\end{description}");
            builder.append("\r\n");
        } catch (Exception ex) {
            if (ex instanceof ServiceException)
                throw ex;
            ExceptionHandler.throwServiceException(ex, logger, ExceptionHandler.UNKNOWN_SERVICE_EXCEPTION_CODE, null);
        }*/
    }

    /**
     * Exports a option of a question
     *
     * @param builder
     * @param option
     * @param index
     * @param targetFolder
     */

    private void exportOption(@NotNull StringBuilder builder, @NotNull QuestionOptionPO option, @Min(0) int index, @NotBlank String targetFolder) {
        var prefix = getOptionPrefix(index);
        var optionText = option.getOptionHypertext();
        builder.append("\\item " + prefix + convertToLatexText(optionText, false, targetFolder));
        builder.append("\r\n");
    }

    /**
     * Gets option prefix
     *
     * @param index
     * @return
     */

    private String getOptionPrefix(@Min(0) int index) {
        var prefix = "";
        switch (index) {
            case 1:
                prefix = "A. ";
                break;
            case 2:
                prefix = "B. ";
                break;
            case 3:
                prefix = "C. ";
                break;
            case 4:
                prefix = "D. ";
                break;
            case 5:
                prefix = "E. ";
                break;
            default:
                prefix = "";
        }

        return prefix;
    }

    /**
     * Converts to latex text
     * 将题库格式文本转换回latex格式文本
     *
     * @param text
     * @param isQuestionText
     * @param targetFolder
     * @return
     */

    private String convertToLatexText(@NotBlank String text, boolean isQuestionText, @NotBlank String targetFolder) {
        String temp = text;
        temp = temp.replace("<span class=\"math-tex\">\\(", "\\(");
        temp = temp.replace("\\)</span>", "\\)");
        temp = temp.replace("<span class=\"math-tex\">\\[", "\\[");
        temp = temp.replace("\\]</span>", "\\]");
        if (temp.contains("<img"))
            temp = downloadImages(temp, isQuestionText, targetFolder);
        temp = temp.replace("<p>", "");
        temp = temp.replace("</p>", "");
        temp = temp.replace("\\lt", " < ");
        temp = temp.replace("\\gt", " > ");
        temp = StringEscapeUtils.unescapeHtml4(temp);
        return temp;
    }

    /**
     * Downloads images in a question hypertext
     * 导出tex文件时，下载并处理题目中有图片的情况
     *
     * @param originalText
     * @param isQuestionText
     * @param targetFolder
     * @return
     */

    private String downloadImages(@NotBlank String originalText, boolean isQuestionText, @NotBlank String targetFolder) {
        var html = "<p>" + originalText + "</p>";
        var doc = Jsoup.parse(html);
        var elements = doc.select("img");
        for (var e : elements) {
            var style = e.attr("style");
            boolean isBig = style.contains("px");
            e.removeAttr("style");
            e.removeAttr("alt");
            var url = e.attr("src");
            String[] s = url.split("/");
            var fileName = s[s.length - 1];
            // String localFile = downloadFile(url, fileName);
            var localFile = IOHelper.downloadFile(url, targetFolder, fileName);
            var imageSize = ImageHelper.getImageSize(localFile);
            int height = imageSize.getHeight();
            if (height >= 130) {
                e.attr("src", "\\newline \\includegraphics[height=2.5cm]{" + fileName + "} \\newline");
            }
            if (height < 130 && height >= 80) {
                e.attr("src", "\\newline \\includegraphics[height=1.5cm]{" + fileName + "}");
            }
            if (height < 80) {
                e.attr("src", "\\includegraphics[height=1.0cm]{" + fileName + "}");
            }
        }
        String temp = doc.body().toString();
        temp = temp.replace("<body>", "");
        temp = temp.replace("</body>", "");
        temp = temp.replace("<p>", "");
        temp = temp.replace("</p>", "");
        temp = temp.replace("<img src=\"", "");
        // temp = temp.replace("\"/>", "");
        // temp = temp.replace("\" />", "");
        temp = temp.replace("\">", "");
        temp = temp.replace("\n", "");
        // temp = temp.replace("<span class=\"math-tex\\(", "\\(");
        // temp = temp.replace("<span class=\"math-tex\\[", "\\[");
        return temp;
    }

    /**
     * Copies a problem set
     * 复制习题集
     *
     * @param setId
     * @return
     */

    @HandleServiceException
    public ProblemSet copyProblemSet(@Min(0) long setId, long operatorId) {
        var oldSet = getProblemSet(setId);
        if (oldSet == null) return null;
        var text = CodeUtils.get36RadixUpperDateTime();
        var newSet = new ProblemSet();
        newSet.setSetTypeId(oldSet.getSetTypeId());
        newSet.setSetCode(oldSet.getSetCode() + text);
        newSet.setProblemQuantity(oldSet.getProblemQuantity());
        newSet.setSetName(oldSet.getSetName() + text);
        newSet.setTotalScore(oldSet.getTotalScore());
        newSet.setTotalTimeLimit(oldSet.getTotalTimeLimit());
        newSet.setDifficulty(oldSet.getDifficulty());
        newSet.setOrganizationId(oldSet.getOrganizationId());
        newSet.setRuleId(oldSet.getRuleId());
        newSet.setSubjectId(oldSet.getSubjectId());
        newSet.setTrueProblem(oldSet.isTrueProblem());
        newSet.setEnabled(oldSet.isEnabled());

        Collection<SetProblem> setProblems = oldSet.getSetProblems();
        Collection<SetProblem> newSetProblems = new ArrayList<>();
        for (var item : setProblems) {
            var newSetProblem = new SetProblem();
            newSetProblem.setSetId(item.getSetId());
            newSetProblem.setProblemScore(item.getProblemScore());
            newSetProblem.setProblemId(item.getProblemId());
            newSetProblem.setProblemIndex(item.getProblemIndex());
            newSetProblems.add(newSetProblem);
        }

        newSet.setSetProblems(newSetProblems);
        return createProblemSet(newSet, operatorId);
    }

    /**
     * Creates a empty problem set for assessment
     * 创建不包含题目的空习题集（用于动态测评场景下动态向习题集内添加题目）
     *
     * @param referenceId
     * @return
     */
    @HandleServiceException
    public ProblemSet createEmptyAssessmentProblemSet(@Min(0) long referenceId, @NotBlank String setName) {
        var dateFormatter = new java.text.SimpleDateFormat("yyMMddHHmmss");
        var code = dateFormatter.format(new Date());
        int exerciseType = SetTypes.ASSESSMENT.getValue();
        // create exercise set
        var set = new ProblemSet();
        set.setSetCode("PS" + code);
        set.setSetName(setName);
        set.setSetTypeId(exerciseType);
        set.setOrganizationId(GlobalState.ORGANIZATION_ID);
        set.setSubjectId(GlobalState.SUBJECT_MATH_ID);
        set.setTotalScore(0);
        set.setTotalTimeLimit(0);
        set.setProblemQuantity(0);
        set.setDifficulty(0);
        set.setTrueProblem(false);
        set.setRuleId(0);
        set.setCreatorId(0);
        set.setCreationTime(LocalDateTime.now());
        set.setModifierId(0);
        set.setModificationTime(LocalDateTime.now());
        set.setReferenceId(referenceId);
        return createProblemSet(set, 0);
    }

    /**
     * Gets maximum item index of a problem set
     *
     * @param setId
     * @return
     */
    @HandleServiceException
    public int getMaxProblemIndex(@Min(0) long setId) {
        return problemSetRepository.getMaxProblemIndex(setId);
    }
    // endregion

}
